//
// See the file COPYING for redistribution information.
//

#include <config.h>
#include <cstdio>
#include <cctype>               // tolower()
#include <algorithm>            // transform()
#include <cstdlib>
#include <pcrecpp.h>
#include <time.h>
#include <Naipax/Version.hpp>
#include <Naipax/Common.hpp>
#include <Naipax/Error.hpp>
#include <Naipax/Http.hpp>

using namespace std;
using namespace Naipax;

// The definition of a token in HTTP headers is seen in Sec. 2.2, RFC
// 2616.
#define TOKEN_REGEXP "[^()<>@,;:\\\"/\\[\\]?={}\\s\\t\\r\\n\\c[]"

#define IS_HEX(c) (((c) >= '0' && ((c) <= '9')) \
                   || ((c) >= 'a' && ((c) <= 'f')) \
                   || ((c) >= 'A' && ((c) <= 'F')))

void _FindLineLength(const string &rData, const uint32_t Offset,
                     uint32_t &rLineLength, uint32_t &rNextLineStart) {
    for (uint32_t i = Offset; i < rData.length(); ++i) {
        if (rData[i] == '\n') {
            rLineLength = i;
            rNextLineStart = i + 1;
            break;
        }
        else if (rData[i] == '\r') {
            if (i + 1 == rData.length()) {
                NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
            }
            if (rData[i+1] == '\n') {
                rLineLength = i;
                rNextLineStart = i + 2;
                break;
            }
        }
    }
}

void HttpRequest::Build(const char *pHost, const char *pPath) {
    Reset();

    if (pHost == NULL || pPath == NULL) {
        NAIPAX_THROW(Error::INVALID_ARGUMENT);
    }
    if (strlen(pHost) >= URL_HOST_SIZE
        || strlen(pPath) >= URL_PATH_SIZE) {
        NAIPAX_THROW(Error::DATA_OUT_OF_RANGE);
    }

    char ReqBuf[HTTP_REQUEST_HEADERS_SIZE];
    // The formal line terminator is CRLF. However, since web servers
    // are suggested to be tolerant, using "\n" can save a little bit
    // bandwidth.
    snprintf(ReqBuf, HTTP_REQUEST_HEADERS_SIZE,
             "GET %s HTTP/1.1\n"
             "Host: %s\n"
             "User-Agent: %s\n"
             "Accept: text/html,text/xml\n"
             "Accept-Charset: UTF-8,*\n"
             "Accept-Encoding: %s\n"
             "Cache-Control: max-age=0\n"
             "\n",
             pPath, pHost,
             (_mUserAgent.length() ?
              _mUserAgent.c_str() : "Naipax/" NAIPAX_VERSION),
             (_mAcceptGzip ? "gzip,*" : "*"));
    _mHeaders = ReqBuf;
}

void HttpRequest::Reset() {
    _mHost = "";
    _mPath = "";
    _mHeaders = "";
    _mAcceptGzip = false;
}

void HttpResponse::_ParseDate(string &rDateStr) {
    // The first format is preferred as an Internet standard and
    // represents a fixed-length subset of that defined by RFC 1123
    // (an update to RFC 822). The second format is in common use, but
    // is based on the obsolete RFC 850 date format and lacks a
    // four-digit year. (Sec. 3.3.1, RFC 2616)

    struct tm Tm = { -1,-1,-1,-1,-1,-1,-1,-1 };
    char *pRet = NULL;

    static const char RFC_822_FORMAT[] = "%a, %d %b %Y %H:%M:%S %z";
    static const char RFC_850_FORMAT[] = "%A, %d-%b-%y %H:%M:%S %z";
    static const char ASCTIME_FORMAT[] = "%a %b %d %H:%M:%S %Y";
    
    // RFC 822
    pRet = strptime(rDateStr.c_str(), RFC_822_FORMAT, &Tm);
    if (pRet == NULL && Tm.tm_sec != -1) {
        mDate = mktime(&Tm);
        mDateStr = rDateStr;
        return;
    }

    // RFC 850
    Tm.tm_sec = -1;
    pRet = strptime(rDateStr.c_str(), RFC_850_FORMAT, &Tm);
    if (pRet == NULL && Tm.tm_sec != -1) {
        mDate = mktime(&Tm);
        mDateStr = rDateStr;
        return;
    }
    
    // ANSI C's asctime() format
    Tm.tm_sec = -1;
    pRet = strptime(rDateStr.c_str(), ASCTIME_FORMAT, &Tm);
    if (pRet == NULL && Tm.tm_sec != -1) {
        mDate = mktime(&Tm);
        mDateStr = rDateStr;
        return;
    }

    NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
}

uint32_t HttpResponse::_GetChunkSize(const char *pLine, uint32_t Len) {
    if (pLine == NULL || Len == 0) {
        NAIPAX_THROW(Error::INVALID_ARGUMENT);
    }

    uint32_t Size = 0;
    for (uint32_t i = 0; i < Len; ++i) {
        if (IS_HEX(pLine[i])) {
            if (i >= 8) {     // 2^32-1 = 0xFFFFFFFF (8 characters)
                NAIPAX_THROW(Error::DATA_OUT_OF_RANGE);
            }
            
            Size <<= 4;
            Size += (pLine[i] >= '0' && pLine[i] <= '9' ?
                     pLine[i] - '0' : (pLine[i] >= 'a' && pLine[i] <= 'f' ?
                                       pLine[i] - 'a' + 10 :
                                       (pLine[i] >= 'A' && pLine[i] <= 'F' ?
                                        pLine[i] - 'A' + 10 : 0)));
        }
        else {
            break;
        }
    }
    return Size;
}

void HttpResponse::_JoinChunks(string &rData) {
    // This algorithm is taken from Sec. 19.4.6, RFC 2616, with some
    // simplifications. This implementation does not process
    // chunk-extensions, and The entity headers after chunk data are
    // ignored.

    uint32_t ChunkSize = 0;
    uint32_t LineLength = 0;
    uint32_t NextLineStart = 0;
    uint32_t Offset = 0;
    string JoinedData;

    while (1) {
        // Get the line of first chunk line
        _FindLineLength(rData, Offset, LineLength, NextLineStart);

        // Get chunk size
        ChunkSize = _GetChunkSize(rData.c_str() + Offset, LineLength);
        if (ChunkSize == 0) {
            break;
        }

        Offset = NextLineStart;

        // Read chunk data
        JoinedData += string(rData, Offset, ChunkSize);
        Offset += ChunkSize;
        
        // Read CRLF after chunk data
        if (Offset < rData.length()) {
            if (rData[Offset] == '\n') {
                ++Offset;
            }
            else if (rData[Offset] == '\r') {
                if (Offset + 1 < rData.length()
                    && rData[Offset + 1] == '\n') {
                    Offset += 2;
                }
                else {
                    NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
                }
            }
        }
        else {
            NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
        }
    }
    rData = JoinedData;
    mIsChunkedTransfer = false;
}

void HttpResponse::_GunzipData(string &rData) {
    // uncompress data here
    mIsGzipped = false;
}

uint32_t HttpResponse::GetStatusCode(const char *pStatusLine,
                                     uint32_t Len) {
    if (pStatusLine == NULL) {
        NAIPAX_THROW(Error::INVALID_ARGUMENT);
    }
    // Find the first non LWS character
    uint32_t Start = 0;
    for (uint32_t i = 0; i <= Len; ++i) {
        if (pStatusLine[i] != ' ' && pStatusLine[i] != '\t') {
            if (pStatusLine[i] == '\r' || pStatusLine[i] == '\n') {
                return 0;
            }
            Start = i;
            break;
        }
    }

    // If HTTP version exists.
    if (Start + 5 <= Len && strncmp(pStatusLine + Start, "HTTP/", 5) == 0) {
        Start += 5;
        // Find the next LWS
        for (uint32_t i = Start; i <= Len; ++i) {
            if (pStatusLine[i] == ' ' || pStatusLine[i] == '\t') {
                Start = i;
                break;
            }
        }
        // Find the next non-LWS
        for (uint32_t i = Start; i <= Len; ++i) {
            if (pStatusLine[i] != ' ' && pStatusLine[i] != '\t') {
                if (pStatusLine[i] == '\r' || pStatusLine[i] == '\n') {
                    return 0;
                }
                Start = i;
                break;
            }
        }

        if (Start + 2 <= Len
            && pStatusLine[Start] >= '1' && pStatusLine[Start] <= '9'
            && pStatusLine[Start + 1] >= '0' && pStatusLine[Start + 1] <= '9'
            && pStatusLine[Start + 2] >= '0' && pStatusLine[Start + 2] <= '9') {
            char Buf[4];
            strncpy(Buf, pStatusLine + Start, 3);
            Buf[3] = '\0';
            return (uint32_t) atol(Buf);
        }
    }

    return 0;
}

uint32_t HttpResponse::GetStatusCode(string &rStatusLine) {
    return HttpResponse::GetStatusCode(rStatusLine.c_str(),
                                       rStatusLine.length());
}

void HttpResponse::ParseStatusLine() {
    if (mRespStr.length() == 0) {
        NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
    }

    pcrecpp::RE
        StatusRegexp("(\\s*(HTTP/\\d+\\.\\d+)\\s(\\d{3})\\s.+?\\r?\\n)",
                     pcrecpp::RE_Options(PCRE_DOTALL));
    string StatusLine;
    if (StatusRegexp.PartialMatch(mRespStr, &StatusLine,
                                  &mHttpVersion, &mStatus) != true) {
        NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
    }

    mHeaderStartPos = (uint32_t) StatusLine.length();
}

void HttpResponse::ParseHeaders() {
    if (mRespStr.length() == 0) {
        NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
    }

    // The field value MAY be preceded by any amount of LWS, though a
    // single SP is preferred. (Sec. 4.2, RFC 2616)
    // 
    // No leading or trailing LWS in field values.
    //
    pcrecpp::RE HeaderRegexp("(" TOKEN_REGEXP "+):\\s*(.+)\\s*");
    uint32_t Offset = mHeaderStartPos;
    while (1) {
        if (Offset == mRespStr.length()) {
            return;
        }
        // Reaching the end of headers?
        if (mRespStr[Offset] == '\n') {
            if (Offset + 1 < mRespStr.length()) {
                mHasBody = true;
                mBodyStartPos = Offset + 1;
                return;
            }
            else {
                NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
            }
        }
        else if (mRespStr[Offset] == '\r'
                 && mRespStr[Offset + 1] == '\n') {
            if (Offset + 2 < mRespStr.length()) {
                mHasBody = true;
                mBodyStartPos = Offset + 2;
                return;
            }
            else {
                NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
            }
        }
        
        if (Offset > mRespStr.length()) {
            NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
        }

        string HeaderLine;
        string Name;
        string Value;
        for (uint32_t i = Offset; i < mRespStr.length(); ++i) {
            if (mRespStr[i] == '\n') {
                HeaderLine = string(mRespStr, Offset, i - Offset);
                Offset = i + 1;
                break;
            }
            else if (mRespStr[i] == '\r' && mRespStr[i+1] == '\n') {
                HeaderLine = string(mRespStr, Offset, i - Offset);
                Offset = i + 2;
                break;
            }
        }

        if (HeaderRegexp.PartialMatch(HeaderLine, &Name, &Value) != true) {
            NAIPAX_THROW(Error::CANNOT_BUILD_DATA);
        }
        
        // Field names are case-insensitive. (Sec. 4.2, RFC 2616)
        transform(Name.begin(), Name.end(), Name.begin(), ::tolower);

        switch (Name[0]) {
            case 'c' : {
                if (Name == "content-length") {
                    mHasContentLength = true;
                    // FIXME: Value is not checked for validity
                    mContentLength = (uint32_t) atol(Value.c_str());
                }
                else if (Name == "content-encoding") {
                    transform(Value.begin(), Value.end(),
                              Value.begin(), ::tolower);
                    if (Value ==  "gzip") {
                        mIsGzipped = true;
                    }
                }
                else if (Name == "content-type") {
                    pcrecpp::RE MediaTypeRegexp("(" TOKEN_REGEXP
                                                "+/" TOKEN_REGEXP "+)");
                    if (MediaTypeRegexp.PartialMatch(Value,
                                                     &mMediaType) != true) {
                        mMediaType = "text/html";
                    }
                    
                    pcrecpp::RE
                        CharsetRegexp("charset=(" TOKEN_REGEXP "+);?");
                    if (CharsetRegexp.PartialMatch(Value,
                                                   &mCharset) != true) {
                        mCharset = "iso-8859-1";
                    }
                }
                break;
            }
            case 'd' : {
                if (Name == "date") {
                    _ParseDate(Value);
                }
                break;
            }
            case 's' : {
                if (Name == "server") {
                    mServer = Value;
                }
                break;
            }
            case 'l' : {
                if (Name == "location") {
                    mLocation = Value;
                }
                break;
            }
            case 't' : {
                if (Name == "transfer-encoding") {
                    transform(Value.begin(), Value.end(),
                              Value.begin(), ::tolower);
                    if (Value == "chunked") {
                        mIsChunkedTransfer = true;
                    }
                }
                break;
            }
            default : {
                if (mStoreOtherHeaders == true) {
                    mOtherHeaders[Name] = Value;
                }
            }
        }
    }
}

void HttpResponse::RestoreBody() {
    if (mIsChunkedTransfer == true || mIsGzipped == true) {
        string Data = string(mRespStr, mBodyStartPos);
        if (Data.length() == 0) {
            return;
        }
        if (mIsChunkedTransfer == true) {
            _JoinChunks(Data);
        }
        if (mIsGzipped == true) {
            _GunzipData(Data);
        }
        mRespStr.replace(mBodyStartPos,
                         mRespStr.length() - mBodyStartPos, Data);
    }
}

void HttpResponse::Reset() {
    mRespStr = "";
    mHttpVersion = "";
    mStatus = (uint32_t) -1;
    mHeaderStartPos = (uint32_t) -1;
    mBodyStartPos = (uint32_t) -1;
    mHasContentLength = false;
    mContentLength = 0;
    mServer = "";
    mMediaType = "";
    mCharset = "";
    mIsChunkedTransfer = false;
    mIsGzipped = false;
    mDateStr = "";
    mDate = 0;
    mLocation = "";
    mStoreOtherHeaders = true;
    mOtherHeaders.clear();
}
